--[[
    热更只支持更新 lua 模块，也就是说，热更的文件的必须是一个模块文件，(热更前和热更后都)必须返回一个表，这保证了对外(除全局变量)只有模块使用到的值才是有意义的值

    热更时，会将 package.loaded[module_name] = nil, 并且重新加载新模块，
    然后递归遍历新模块表，把新模块中的值设置到老模块上，也就是说，把老模块刷新成了新模块:
        对于新模块中的 function, 如果他有上值，那么尝试在老模块查找相同名字的上值，并将其上值绑定为老模块上值，以保留上下文，
        对于新模块中的 table, 也会和模块一样，执行递归遍历，并且将新值更新到老模块中: old_module[k] = update_table(new_table, old_table)
        对于新模块中其他非引用类型的值，更新其值不会直接反应到模块外的引用，所以，如果要热更，那么我们必须在其他使用时使用 M.key，而不能将其存入到一个局部变量中
        建议将 M.key 的引用存入到一个局部变量中，然后在模块中操作这个值，也就是说，为这个值添加 get/set 函数，这样，每次使用都会获取到最新的值，而不用担心热更带来的问题

        对于全局变量，如果我们想保留热更前的值，可以使用 A = A 来保留其值

    对于一些特殊情况，Hotfix 提供了两个钩子一个热更元信息表(__HOTFIX)
        refresh: 如果一个上值需要更新为新的上值时，设置 refresh[up_name] = true，每个需要更新的表都可以有这个元字段，默认会绑定到老模块上值
        keep: 如果一个表中的字段需要保留为老模块的值时，设置 keep[k] = true，每个需要更新的表都可以有这个元字段，默认会使用新的模块值
        prefix_cb: 在热更前调用，参数是 (new_module, old_module)
        suffix_cb: 在热更后调用，参数是 (更新后的模块)

    通常，你可以只使用 prefix_cb 和 suffix_cb 来达到目的, 而不需要考虑 refresh 和 keep 有些曲折的实现。

    建议的可热更模块书写方式:
    local M = {}

    local up_value = 1
    -- 将函数放入 M 中，以保证上值的刷新
    function M.set_up_value(v)
        up_value = v
    end

    function M.get_up_value(v)
        return up_value
    end

    return M
]]

local debug = debug
local assert = assert
local package = package
local type = type
local pairs = pairs
local string_format = string.format
local setupvalue = debug.setupvalue
local getupvalue = debug.getupvalue

local M = {}

-- obj = true
local updated = {
    [_ENV] = true
}

-- upvalue = true
local refresh_upv = {}
local MAX_UPV_COUNT = 255

local update_func, update_table
update_func = function(new_func, old_func, name)
    assert(type(new_func) == "function" and type(old_func) == "function", "hotfix invalid function")
    if name then
        if updated[name] then
            return
        end

        updated[name] = true
    end

    local old_func_upvs = {}
    -- Lua 中最多 255 个上值
    for i = 1, MAX_UPV_COUNT do
        local up_name, value = getupvalue(old_func, i)
        if up_name == nil then
            break
        end

        if up_name ~= "_ENV" then
            old_func_upvs[up_name] = value
        end
    end

    for i = 1, MAX_UPV_COUNT do
        local up_name, up_val = getupvalue(new_func, i)
        if up_name == nil then
            break
        end

        if not updated[up_name] and not refresh_upv[up_name] then
            local old_value = old_func_upvs[up_name]
            if old_value and up_val ~= old_value then
                -- print("updateupvalue", string_format("updateupvalue %s %s -> %s", up_name, old_value, up_val))
                local old_val_type = type(old_value)
                if old_val_type ~= type(up_val) then
                    setupvalue(new_func, i, old_value)
                elseif old_val_type == "function" then
                    update_func(new_func, old_value, up_name)
                elseif old_val_type == "table" then
                    -- update_table(up_val, old_value, up_name)
                    setupvalue(new_func, i, old_value)
                else
                    setupvalue(new_func, i, old_value)
                end
            end
        end
    end
end

update_table = function(new_tbl, old_tbl, name)
    assert("table" == type(new_tbl) and "table" == type(old_tbl))
    if updated[name] then
        return
    end

    updated[name] = true
    local hotfix_meta = new_tbl.__HOTFIX
    local keep_keys = hotfix_meta and hotfix_meta.keep or {}
    for k, v in pairs(new_tbl) do
        if not keep_keys[k] then
            local k_type = type(k)
            local v_type = type(v)
            local old_value = old_tbl[k]
            if v_type ~= type(old_value) then
                old_tbl[k] = v
            elseif v_type == "function" then
                update_func(v, old_value, name .. "." .. k)
                old_tbl[k] = v
            elseif v_type == "table" then
                -- 如果是函数，则需要更新upvalue
                update_table(v, old_tbl[k], name .. "." .. k)
            else
                old_tbl[k] = v
            end
        end
    end

    -- Update metatable.
    local old_meta = debug.getmetatable(old_tbl)
    local new_meta = debug.getmetatable(new_tbl)
    if old_meta and new_meta then
        update_table(new_meta, old_meta)
    end
end

local function clear_updated()
    updated = {
        _ENV = true
    }
end

local function clear_refresh()
    refresh_upv = {}
end

function M.hotfix(module_name)
    clear_updated()
    clear_refresh()
    local old_module = package.loaded[module_name]
    assert(type(old_module) == "table", string_format("hotfix module: %s error, old_module is not table", module_name))
    if not package.loaded[module_name] then
        return
    end

    local old_module_type = type(old_module)
    package.loaded[module_name] = nil

    local ok, new_module = pcall(require, module_name)
    if not ok then
        print(string_format("hotfix error: %s, err: %s", module_name, new_module))
        return
    end

    -- 只允许更新 table 类型，table 可以存储任意类型，如果需要返回其他值，使用 table 包装起来
    assert(type(new_module) == "table", string_format("hotfix module: %s error, new_module is not table", module_name))

    -- print("oldmodule", old_module, table.dump(old_module, true)) -- for debug
    -- print("newmodule", new_module, table.dump(new_module, true))

    local fix_meta = new_module.__HOTFIX
    if fix_meta and fix_meta.refresh then
        refresh_upv = table.merge(refresh_upv, fix_meta.refresh)
    end

    -- 上值中需要刷新的值
    if fix_meta and fix_meta.prefix_cb then
        fix_meta.prefix_cb(new_module, old_module)
    end

    update_table(new_module, old_module, module_name)
    if fix_meta and fix_meta.suffix_cb then
        fix_meta.suffix_cb(old_module)
    end

    -- 将 module 放回原处
    package.loaded[module_name] = old_module
end

M.__HOTFIX = {
    prefix_cb = function(new_module, old_module)
        -- print("hotfix module: hotfix")
    end,
    suffix_cb = function(module)
        -- print("hotfix module: suffix over")
    end
}

return M
